#!/bin/bash
#
# SPDX-FileCopyrightText: 2021-2025 BlueALSA developers
# SPDX-License-Identifier: MIT
#
# Bash completion for bluez-alsa project applications.

# helper function gets available profiles
# @param $1 the bluealsad executable name
_bluealsa_profiles() {
	"$1" --help | while read -r line; do
		[[ "$line" = "Available BT profiles:" ]] && start=yes && continue
		[[ "$start" ]] || continue
		[[ "$line" ]] || break
		read -ra words <<< "$line"
		echo "${words[1]}"
	done
}

# helper function gets available codecs
# @param $1 the bluealsad executable name
_bluealsa_codecs() {
	"$1" --help | while read -r line; do
		[[ "$line" = "Available BT audio codecs:" ]] && start=yes && continue
		[[ "$start" ]] || continue
		[[ "$line" ]] || break
		line=${line,,}
		IFS=", " read -ra words <<< "${line,,}"
		echo "${words[@]:1}"
	done
}

# helper function gets available enum values
# @param $1 the bluealsad executable name
# @param $2 the bluealsad option to inspect
_bluealsa_enum_values() {
	"$1" "${2}=" 2>&1 | while read -r line; do
		[[ $line =~ \{([^}]*)\} ]] || continue
		echo "${BASH_REMATCH[1]//,/}"
	done
}

# helper function gets dbus services
# note that this function does not detect if default service (no suffix) is running.
_bluealsa_list_dbus_suffices() {
	dbus-send --system --dest=org.freedesktop.DBus --type=method_call \
	          --print-reply /org/freedesktop/DBus \
	          org.freedesktop.DBus.ListNames 2>/dev/null | \
		while read -r line; do
			[[ $line =~ org\.bluealsa\.([^'"']+) ]] || continue
			echo "${BASH_REMATCH[1]}"
		done
}

# helper function gets codecs for given pcm
# before calling this function, make sure that the bautil_args was properly
# initialized with the _bluealsa_util_init function
# @param $1 pcm path
_bluealsa_pcm_codecs() {
	"${bautil_args[@]}" codec "$1" | while read -r line; do
		[[ $line =~ ^Available\ codecs:\ ([^[]+$) ]] || continue
		echo "${BASH_REMATCH[1]}"
	done
}

# helper function completes supported bluealsactl monitor properties
_bluealsactl_properties() {
	local properties=( codec running softvolume volume )
	if [[ "$cur" == *,* ]]; then
		local realcur prefix chosen remaining
		realcur="${cur##*,}"
		prefix="${cur%,*}"
		IFS="," read -ra chosen <<< "${prefix,,}"
		readarray -t remaining < <(printf '%s\n' "${properties[@]}" "${chosen[@]}" | sort | uniq -u)
		if [[ ${#remaining[@]} -gt 0 ]]; then
			readarray -t COMPREPLY < <(compgen -W "${remaining[*]}" -- "$realcur")
			if [[ ${#COMPREPLY[@]} -eq 1 ]] ; then
				COMPREPLY[0]="$prefix,${COMPREPLY[0]}"
			fi
			if [[ ${#remaining[@]} -gt 0 && "$cur" == "${COMPREPLY[0]}" ]] ; then
				COMPREPLY=( "${COMPREPLY[0]}," )
			fi
			if [[ ${#remaining[@]} -gt 1 ]]; then
				compopt -o nospace
			fi
		fi
	else
		readarray -t COMPREPLY < <(compgen -W "${properties[*]}" -- "$cur")
		if [[ ${#COMPREPLY[@]} -eq 1 && "$cur" == "${COMPREPLY[0]}" ]]; then
			COMPREPLY=("${COMPREPLY[0]},")
		fi
		compopt -o nospace
	fi
}

# helper function gets ALSA pcms
# @param $1 is the current word to match
_bluealsa_aplay_pcms() {
	eval local cur="$1"
	cur="${cur/%\\/\\\\}"
	while read -r; do
		[[ "$REPLY" == " "* ]] && continue
		[[ "$REPLY" == "$cur"* ]] || continue
		echo "${REPLY// /\\ }"
	done < <(aplay -L 2>/dev/null)
}

# helper function gets rfcomm dbus paths
# @param $1 the full BlueALSA service name ( org.bluealsa* )
_bluealsa_rfcomm_paths() {
	busctl --list tree "$1" 2>/dev/null | while read -r line; do
		[[ "$line" = /org/bluealsa/hci[0-9]/dev*/rfcomm ]] || continue
		echo "${line/\/rfcomm/}"
	done
}

# helper function gets options from command line
# @param $1 the executable name
# puts exec and dbus arg ( --dbus=aaa ) into array variable bautil_args
# puts service name ( org.bluealsa.aaa ) into variable service
# puts offset of first non-option argument into variable nonopt_offset
# @return 0 if no errors found, 1 if dbus check failed
_bluealsa_util_init() {
	local valopts="-B --dbus"
	bautil_args=( "$1" )
	service="org.bluealsa"
	nonopt_offset=0
	local i
	for (( i=1; i <= COMP_CWORD; i++ )); do
		case "${COMP_WORDS[i]}" in
			--dbus)
				if (( i == COMP_CWORD )) ; then
					break
				elif (( i == COMP_CWORD - 1 )) ; then
					[[ "${COMP_WORDS[i+1]}" = = ]] && break
					bautil_args+=( "--dbus=${COMP_WORDS[i+1]}" )
					service="org.bluealsa.${COMP_WORDS[i+1]}"
					break
				else
					[[ "${COMP_WORDS[i+1]}" = = ]] && (( i++ ))
					if [[ "${COMP_WORDS[i+1]}" ]] ; then
						(( i++ ))
						bautil_args+=( "--dbus=${COMP_WORDS[i]}" )
						service="org.bluealsa.${COMP_WORDS[i]}"
						continue
					fi
				fi
				;;
			-B)
				if (( i == COMP_CWORD )) ; then
					break
				elif (( i == COMP_CWORD - 1 )) ; then
					bautil_args+=( "--dbus=${COMP_WORDS[i+1]}" )
					service="org.bluealsa.${COMP_WORDS[i+1]}"
					break
				else
					if [[ "${COMP_WORDS[i+1]}" ]] ; then
						(( i++ ))
						bautil_args+=( "--dbus=${COMP_WORDS[i]}" )
						service="org.bluealsa.${COMP_WORDS[i]}"
						continue
					fi
				fi
				;;
			-*|=)
				continue
				;;
		esac

		[[ "${COMP_WORDS[i-1]}" == = ]] && (( i < COMP_CWORD )) && continue
		[[ "$valopts " == *"${COMP_WORDS[i-1]} "* ]] && continue

		nonopt_offset=$i
		break

	done

	return 0
}

# helper function completes options
_bluealsa_complete_options() {
	readarray -t COMPREPLY < <(compgen -W "$(_parse_help "$1")" -- "$cur")
	[[ ${COMPREPLY[0]} == *= ]] && compopt -o nospace
}

# completion function for bluealsad
# complete available devices and profiles in addition to options
_bluealsad() {

	local cur prev words cword split
	_init_completion -s || return

	local prefix list

	case "$prev" in
		--device|-i)
			readarray -t list < <(ls -I '*:*' /sys/class/bluetooth)
			readarray -t COMPREPLY < <(compgen -W "${list[*]}" -- "$cur")
			return
			;;
		--profile|-p)
			[[ $cur =~ ^[+-].* ]] && prefix=${cur:0:1} && cur=${cur:1}
			readarray -t list < <(_bluealsa_profiles "$1")
			readarray -t COMPREPLY < <(compgen -P "$prefix" -W "${list[*]}" -- "$cur")
			return
			;;
		--codec|-c)
			[[ $cur =~ ^[+-].* ]] && prefix=${cur:0:1} && cur=${cur:1}
			readarray -t list < <(_bluealsa_codecs "$1")
			readarray -t COMPREPLY < <(compgen -P "$prefix" -W "${list[*]}" -- "$cur")
			return
			;;
		--loglevel|--sbc-quality|--aac-latm-version|--mp3-algorithm|--mp3-vbr-quality|--ldac-quality)
			readarray -t list < <(_bluealsa_enum_values "$1" "$prev")
			readarray -t COMPREPLY < <(compgen -W "${list[*]}" -- "$cur")
			return
			;;
		--xapl-resp-name)
			readarray -t COMPREPLY < <(compgen -W "BlueALSA iPhone" -- "$cur")
			return
			;;
		--*)
			[[ ${COMP_WORDS[COMP_CWORD]} = = ]] && return
			;;
	esac

	_bluealsa_complete_options "$1"
}

# completion function for bluealsa-aplay
# completes available dbus suffices and ALSA pcms in addition to options
# - does not complete MAC addresses
# requires aplay to list ALSA pcms
_bluealsa_aplay() {

	local cur prev words cword split
	_init_completion -s -n : || return

	local list

	case "$prev" in
		--dbus|-B)
			_have dbus-send || return
			readarray -t list < <(_bluealsa_list_dbus_suffices)
			readarray -t COMPREPLY < <(compgen -W "${list[*]}" -- "$cur")
			return
			;;
		--pcm|-D)
			_have aplay || return

			# do not attempt completion on words containing ' or "
			[[ "$cur" =~ [\'\"] ]] && return

			readarray -t COMPREPLY < <(_bluealsa_aplay_pcms "$cur")

			# ALSA pcm names can contain '=' and ':', both of which cause
			# problems for bash completion if it considers them to be word
			# terminators. So we adjust the list of candidate matches to allow
			# for this.
			if [[ "$cur" == *=* && "$COMP_WORDBREAKS" == *=* ]]; then
				# Remove equal-word prefix from COMPREPLY items
				local equal_prefix=${cur%"${cur##*=}"}
				local i=${#COMPREPLY[*]}
				while [[ $((--i)) -ge 0 ]]; do
					COMPREPLY[i]=${COMPREPLY[$i]#"$equal_prefix"}
				done
			fi
			__ltrim_colon_completions "$cur"
			return
			;;
		--loglevel|--resampler|--volume)
			readarray -t list < <(_bluealsa_enum_values "$1" "$prev")
			readarray -t COMPREPLY < <(compgen -W "${list[*]}" -- "$cur")
			return
			;;
		--*)
			[[ ${COMP_WORDS[COMP_CWORD]} = = ]] && return
			;;
	esac

	_bluealsa_complete_options "$1"
}

# completion function for bluealsactl
# complete available dbus suffices, command names and pcm paths in addition to options
_bluealsactl() {

	local cur prev words cword split
	_init_completion -s || return

	local bautil_args service
	local -i nonopt_offset
	_bluealsa_util_init "$1" || return

	# the command names supported by this version of bluealsactl
	local simple_commands="list-pcms list-services monitor status"
	local path_commands="codec client-delay info mute open soft-volume volume"

	# options that may appear before or after the command
	local global_shortopts="-h"
	local global_longopts="--help"

	# options that may appear only before the command
	local base_shortopts="-B -V -q -v"
	local base_longopts="--dbus= --version --quiet --verbose"

	local command path list

	# process pre-command options
	case "$nonopt_offset" in
		0)
			case "$prev" in
				--dbus|-B)
					_have dbus-send || return
					readarray -t list < <(_bluealsa_list_dbus_suffices)
					readarray -t COMPREPLY < <(compgen -W "${list[*]}" -- "$cur")
					return
					;;
			esac
			case "$cur" in
				-|--*)
					readarray -t COMPREPLY < <(compgen -W "$global_longopts $base_longopts" -- "$cur")
					[[ "${COMPREPLY[0]}" == *= ]] && compopt -o nospace
					;;
				-?)
					readarray -t COMPREPLY < <(compgen -W "$global_shortopts $base_shortopts" -- "$cur")
					;;
			esac
			return
			;;
		"$COMP_CWORD")
			# list available commands
			readarray -t COMPREPLY < <(compgen -W "$simple_commands $path_commands" -- "$cur")
			return
			;;
	esac

	# check for valid command
	command="${COMP_WORDS[nonopt_offset]}"
	[[ "$simple_commands $path_commands" =~ $command ]] || return

	# process command-specific options
	case "$command" in
		monitor)
			case "$prev" in
				--properties)
					_bluealsactl_properties
					return
					;;
			esac
			global_longopts+=" --properties"
			global_shortopts+=" -p"
			case "$cur" in
				-p)
					COMPREPLY=( "--properties" )
					compopt -o nospace
					return
					;;
				-p?*)
					COMPREPLY=( "--properties=${cur:2}" )
					compopt -o nospace
					return
					;;
				--properties)
					COMPREPLY=( "--properties=" )
					compopt -o nospace
					return
					;;
				-)
					COMPREPLY=( "--" )
					compopt -o nospace
					return
					;;
			esac
			;;
	esac

	# find path argument
	for (( i=(nonopt_offset + 1); i < COMP_CWORD; i++ )); do
		[[ "${COMP_WORDS[i]}" == -* ]] && continue
		path="${COMP_WORDS[i]}"
		path_offset=i
		break
	done

	# process global options and path
	if [[ -z "$path" ]] ; then
		case "$cur" in
			-|--*)
				readarray -t COMPREPLY < <(compgen -W "$global_longopts" -- "$cur")
				[[ "${COMPREPLY[0]}" == --properties* ]] && compopt -o nospace
				return
				;;
			-?)
				readarray -t COMPREPLY < <(compgen -W "$global_shortopts" -- "$cur")
				return
				;;
		esac
		if [[ "$path_commands" =~ $command ]]; then
			readarray -t list < <("${bautil_args[@]}" list-pcms 2>/dev/null)
			readarray -t COMPREPLY < <(compgen -W "${list[*]}" -- "$cur")
			return
		fi
	fi

	# process command positional arguments
	case "$command" in
		codec)
			if (( COMP_CWORD == path_offset + 1 )) ; then
				readarray -t list < <(_bluealsa_pcm_codecs "$path")
				readarray -t COMPREPLY < <(compgen -W "${list[*]}" -- "$cur")
			fi
			;;
		mute)
			if (( COMP_CWORD < path_offset + 3 )) ; then
				if [[ "$cur" == "" ]] ; then
					COMPREPLY=( true false )
				else
					readarray -t COMPREPLY < <(compgen -W "on yes true 1 off no false 0" -- "${cur,,}")
				fi
			fi
			;;
		soft-volume)
			if (( COMP_CWORD < path_offset + 2 )) ; then
				if [[ "$cur" == "" ]] ; then
					COMPREPLY=( true false )
				else
					readarray -t COMPREPLY < <(compgen -W "on yes true 1 off no false 0" -- "${cur,,}")
				fi
			fi
			;;
		monitor)
			case "$prev" in
				--properties)
					_bluealsactl_properties
					;;
				-p)
					return
					;;
			esac
			;;
	esac
}

# completion function for bluealsa-rfcomm
# complete available dbus suffices and device paths in addition to options
# requires busctl (part of elogind or systemd) to list device paths
_bluealsa_rfcomm() {

	local cur prev words cword split
	_init_completion -s || return

	local bautil_args service
	local -i nonopt_offset
	_bluealsa_util_init "$1" || return

	local list

	# check for dbus service suffix first
	case "$prev" in
		--dbus|-B)
			_have dbus-send || return
			readarray -t list < <(_bluealsa_list_dbus_suffices)
			readarray -t COMPREPLY < <(compgen -W "${list[*]}" -- "$cur")
			return
			;;
		--*)
			[[ ${COMP_WORDS[COMP_CWORD]} = = ]] && return
			;;
	esac

	if (( nonopt_offset == COMP_CWORD )) ; then
		_have busctl || return
		readarray -t list < <(_bluealsa_rfcomm_paths "$service")
		readarray -t COMPREPLY < <(compgen -W "${list[*]}" -- "$cur")
		return
	fi

	# do not list options if path was found
	(( nonopt_offset > 0 )) && return

	_bluealsa_complete_options "$1"
}

# completion function for a2dpconf and hcitop
# complete only the options
_bluealsa_others() {
	# shellcheck disable=SC2034
	local cur prev words cword split
	_init_completion -s || return
	case "$prev" in
		--*)
			[[ ${COMP_WORDS[COMP_CWORD]} = = ]] && return
			;;
	esac
	_bluealsa_complete_options "$1"
}

complete -F _bluealsad bluealsad
complete -F _bluealsactl bluealsactl
complete -F _bluealsa_aplay bluealsa-aplay
complete -F _bluealsa_rfcomm bluealsa-rfcomm
complete -F _bluealsa_others hcitop
complete -F _bluealsa_others a2dpconf
